using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;
using System.Runtime.InteropServices;
using System.Reflection;

using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Controls;

using IEeeeInterop;

using UKVSTS;
using UKVSTS.FieldDefaultValues;
using Microsoft.Win32;

namespace IEeee
{
    /// <summary>
    /// This is the main starting point for IEeee. This class contains the code that IE
    /// will call into when the button is clicked.
    /// </summary>
    [ComVisible(true)]
    [Guid("FA7FEF8E-2278-4e8c-B7A0-2D3B338FFE78")]
    public class IEeeeButton : IObjectWithSite, Microsoft.VisualStudio.OLE.Interop.IOleCommandTarget
    {

        private SHDocVw.IWebBrowser2 m_ParentBrowser;

        static IEeeeButton()
        {
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
        }

        /// <summary>
        /// I've had a problem with loading some of the dependant dlls.
        /// Rather than try and configure the appdoain to use an alternate code path.
        /// I'm just going to give it a hand.
        /// </summary>
        static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            Assembly assm = null;
            try
            {
                AssemblyName name = new AssemblyName(args.Name);

                // Look in the directory where IEeee.dll got loaded from.
                //
                String ieeeDir = Path.GetDirectoryName(typeof(IEeeeButton).Assembly.Location);
                String assmLocation = Path.Combine(ieeeDir, name.Name + ".dll");

                if (File.Exists(assmLocation))
                {
                    assm = Assembly.LoadFrom(assmLocation);
                }
                else
                {


                    // Ok, now the Microsoft.TeamFoundation.WorkItemTracking.Controls.dll
                    // assembly is a little more difficult. It isn;t in the GAC, but we will need
                    // to load the version of the dll to match the version of the FTS dlls that
                    // are already in memory
                    Version ver = typeof(WorkItem).Assembly.GetName().Version;


                    String keyName = String.Format(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\{0}.{1}",
                                                    ver.Major, ver.MajorRevision);
                    String ideInstallDir = Registry.GetValue(keyName, "InstallDir", null) as String;

                    ideInstallDir = Path.Combine(ideInstallDir, "PrivateAssemblies");

                    
                    assmLocation = Path.Combine(ideInstallDir, name.Name + ".dll");

                    if (File.Exists(assmLocation))
                    {
                        assm = Assembly.LoadFrom(assmLocation);
                    }
                }
            }
            catch (Exception) { }

            return assm;
        }

        // Latch variable to stop people pressing the button more than once.
        // Should realy be a bool, but there is no specailed version of CompaireExchange for 
        // bools - and using the <T> version would require the bool to be boxed.
        // So its an int - i don't like it, but thats what its gonna have to be.
        //
        private static int m_Executing;

        /// <summary>
        /// IE calles this method and passes in the browser instance.
        /// </summary>
        public int SetSite(object site)
        {
            try
            {
                if (site != null)
                {

                    IEeeeInterop.IServiceProvider prov = (IEeeeInterop.IServiceProvider) site;

                    Guid SID_SWebBrowserApp = typeof (SHDocVw.IWebBrowserApp).GUID;

                    Guid IID_IWebBrowser = typeof (SHDocVw.IWebBrowser2).GUID;

                    Object pWebBrowser;

                    prov.QueryService (ref SID_SWebBrowserApp, ref IID_IWebBrowser, out pWebBrowser);
                    m_ParentBrowser = (SHDocVw.IWebBrowser2) pWebBrowser;

                    if (m_ParentBrowser != null)
                    {
                        //                    m_ParentBrowser.DocumentComplete += new DWebBrowserEvents2_DocumentCompleteEventHandler ();
                    }
                }
                else
                {
                    // Ok, we are being closed down and need to Release the site ptr.
                    //
                    if (m_ParentBrowser != null)
                    {
                        //                    m_ParentBrowser.DocumentComplete -= new DWebBrowserEvents2_DocumentCompleteEventHandler(webBrowser_DocumentComplete);
                        m_ParentBrowser = null;
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine ("SetSite : " + ex.ToString ());
            }
            return 0;
        }

        public int GetSite (ref Guid guid, out IntPtr ppvSite)
        {
            try
            {
                Debug.WriteLine ("GetSite " + guid.ToString ());
                ppvSite = IntPtr.Zero;
            }
            catch (Exception ex)
            {
                Debug.WriteLine ("SetSite : " + ex.ToString ());
                throw;
            }
            return 0;
        }

        /// <summary>
        /// Allows IE to query us to see what state we should be in.
        /// As far as this is concerned, we are always enabled and ready to go.
        /// </summary>
        public int QueryStatus (ref Guid pguidCmdGroup, 
                                uint cCmds,
                                Microsoft.VisualStudio.OLE.Interop.OLECMD[] prgCmds,
                                IntPtr pCmdText)
        {
            int hr = COMCodes.OLECMDERR_E_UNKNOWNGROUP;

            try
            {
                Debug.WriteLine("QueryStatus " + pguidCmdGroup);

                if (pguidCmdGroup.Equals(COMCodes.CLSID_ToolbarExtButtons))
                {
                    for (int i = 0; i < prgCmds.Length; i++)
                    {
                        // Let IE know that we are ready to run.
                        //
                        Debug.WriteLine(" - Id = " + prgCmds[i].cmdID);
                        prgCmds[i].cmdf = (int)(OleCmdf.Enabled | OleCmdf.Supported);
                        prgCmds[i].cmdID = 0;
                    }
                    hr = COMCodes.S_OK;
                }
            }
            catch (Exception ex)
            {
                IEeeeException.DisplayError (null, ex);
                throw;
            }
            return hr;
        }


        public int Exec (ref Guid pguidCmdGroup, uint nCmdID, uint nCmdExecOpt, IntPtr pvaIn, IntPtr pvaOut)
        {
            // We are going to gave to delete our temp dir and files later, so these
            // need to be declared outside the try block.
            //
            String dirName = null;
            String fileName = null;

            const int NOT_EXECUTING = 0;
            const int EXECUTING = 1;

            // Flip our latch to show that we are executing this code.
            // Sometime, especiually at startup, it is possible to click the button more than once
            // before the UI displays. This just ensures that the UI only apears once.
            //
            if (System.Threading.Interlocked.CompareExchange (ref m_Executing, EXECUTING, NOT_EXECUTING) == NOT_EXECUTING)
            {
                try
                {
                    // Generate a unique dir where we can store all our temp files.
                    // These files include the screenshot, source files, css, ...
                    //
                    dirName = Path.Combine (Path.GetTempPath (), "IEeee_" + Guid.NewGuid().ToString ());
                    Directory.CreateDirectory (dirName);
                    
                    fileName = Path.Combine (dirName, "ScreenShot.png");

                    // Take the screenshot as soon as we can.
                    //
                    IntPtr hwnd = (IntPtr)m_ParentBrowser.HWND;
                    ScreenCapture.CaptureScreen (hwnd, fileName, System.Drawing.Imaging.ImageFormat.Png);

                    // Now capture as much about IE as we can.
                    //
                    using (IEStateShapshot snapShot = IEStateShapshot.TakeSnapshot (dirName, (mshtml.IHTMLDocument2) m_ParentBrowser.Document))
                    {

                        // Display the bug description form. This asks the user what they 
                        // want to include in the defect report and will also connect us to TFS.
                        //
                        using (BugDescription descDialog = new BugDescription())
                        {
                            descDialog.SetScreenshotDetails(fileName, System.Drawing.Imaging.ImageFormat.Png);
                            
                            // Have a go at creating a sensible title for the bug
                            //
                            if (String.IsNullOrEmpty(snapShot.Url) == false)
                            {
                                descDialog.BugTitle = "Error with " + snapShot.Url;
                            }

                            if (descDialog.ShowDialog () == System.Windows.Forms.DialogResult.OK)
                            {
                                // At this point we are successfully connected to TFS, so
                                // lets create the work item for our defect report.
                                //
                                WorkItemType wiType = IEeeeConfig.Instance.SelectedWorkItemType;
                                WorkItem wi = wiType.NewWorkItem ();

                                // Now lets start populating the work item fields.
                                //

                                // The user has had the chance to provide a description
                                // of the error. we need to make sure that we don't
                                // overwrite this with our brower details.
                                //
                                String desc = descDialog.BugDetails;

                                if (descDialog.InculdeBrowserDetails)
                                {
                                    Field fieldToStoreDescIn = null;
                                    // Append our browser details to the users description.
                                    //
                                    desc = String.Concat(desc, "\n\nBrowser Details\n===============\n\n");
                                    desc = String.Concat(desc, snapShot.GetDescription());

                                    // Has the user requested that the desc go into a field
                                    // other than the standard Description field?
                                    //
                                    if (wi.Fields.Contains(descDialog.WorkItemFieldName))
                                    {
                                        fieldToStoreDescIn = wi.Fields[descDialog.WorkItemFieldName];
                                    }

                                    // Ok, lets check that the requested field is sutable
                                    // for receiving the desc.
                                    //
                                    if (fieldToStoreDescIn == null
                                        || IsFieldSutableToStoreDescription(fieldToStoreDescIn) == false)
                                    {
                                        fieldToStoreDescIn = wi.Fields[CoreField.Description];
                                    }

                                    fieldToStoreDescIn.Value = desc;
                                }
                                else
                                {
                                    // Ok, just the details that the user entered in the
                                    // bug description form.
                                    //
                                    wi.Description = desc;
                                }

                                // Apply any Field Default Values to this new work item.
                                //
                                if (descDialog.ApplyFieldDefaultValues) {

                                    ApplyFieldDefaultValues(wi);
                                }

                                // An easy one ...
                                wi.Title = descDialog.BugTitle;

                                // Does the user want the screenshot?
                                //
                                if (descDialog.InculdeScreenShot)
                                {
                                    wi.Attachments.Add (new Attachment (fileName, "Screen shot"));
                                }

                                // Add a link to the erroring page.
                                // Not sure how useful this will be, but its easy to do.
                                //
                                wi.Links.Add (new Hyperlink (snapShot.Url));


                                // Go through all our temp files and attached all those
                                // the the user has asked for.
                                //
                                foreach (AttachmentDetails detail in snapShot.Attachments)
                                {
                                    bool include = false;

                                    switch (detail.AttachmentType)
                                    {
                                        case AttachmentType.Document:
                                            include = descDialog.InculdeMainSourceFile;
                                            break;
                                        case AttachmentType.Frame:
                                            include = descDialog.InculdeFrames;
                                            break;
                                        case AttachmentType.StyleSheet:
                                            include = descDialog.InculdeStyleSheets;
                                            break;
                                        case AttachmentType.Script:
                                            include = descDialog.InculdeScriptFiles;
                                            break;
                                        case AttachmentType.Image:
                                            break;
                                    }

                                    if (include == true)
                                    {
                                        wi.Attachments.Add (new Attachment (detail.FileName, detail.Name));
                                    }
                                }

                                // Finally display our populated work item in the 
                                // work item editing form.
                                //
                                using (CreateBug form = new CreateBug ())
                                {
                                    form.WorkItem = wi;
                                    form.ShowDialog ();
                                }
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    IEeeeException.DisplayError(null, ex);
                    throw;
                }
                finally
                {
                    // Flip the latch back again.
                    //
                    if (System.Threading.Interlocked.CompareExchange(ref m_Executing, NOT_EXECUTING, EXECUTING) != EXECUTING)
                    {
                        Debug.Assert(false, "m_Executing wasn't EXECUTING");
                    }

                    try
                    {
                        // Clean up any files that we have created.
                        //
                        File.Delete (fileName);
                        
                        // Now do our temp dir, it should be empty at this point.
                        //
                        Directory.Delete(dirName);
                    }
                    catch (IOException)
                    {
                        // Ok, I know I shouldn't, but I'm just going to ignore this.
                    }
                }
            }
            return 0;
        }

        /// <summary>
        /// Applies the Field Default values to the passed work item.
        /// These populate fields in the work item with user defined default values.
        /// If an error occurs, the user is given the option to reove the default
        /// value from the list of default values for this work item type.
        /// </summary>
        /// <param name="wi">The work item that will be populated</param>
        private static void ApplyFieldDefaultValues(WorkItem wi)
        {
            List<FieldDefaultValue> defaultsForThisWorkItem = FieldDefaultValueConfig.Instance.GetDefaultsForWorkItem(wi.Type);

            if (defaultsForThisWorkItem != null)
            {
                // If an error occurs when we apply the default, we might want to
                // remove this for the future. So we will need to remember all
                // the fields that need to be removed.
                //
                List<FieldDefaultValue> erroredDefaultToRemove = new List<FieldDefaultValue>();

                foreach (FieldDefaultValue value in defaultsForThisWorkItem)
                {
                    String reason;
                    if (value.ApplyToWorkItem(wi, out reason) == false)
                    {
                        // If there has been an error, give the user the option
                        // to remove the offending default from the list
                        // of defaults.
                        //
                        if (System.Windows.Forms.MessageBox.Show(
                                                             "Unable to set Field Default Value:\n"
                                                             + reason + "\nWould you like to delete this Field Default Value?",
                                                             "IEeee!", 
                                                             System.Windows.Forms.MessageBoxButtons.YesNo,
                                                             System.Windows.Forms.MessageBoxIcon.Error,
                                                             System.Windows.Forms.MessageBoxDefaultButton.Button2)
                                                    == System.Windows.Forms.DialogResult.Yes)
                        {
                            // Remember this for later.
                            //
                            erroredDefaultToRemove.Add(value);
                        }
                    }
                }

                // Do we have any erroring defaults that we need to remove?
                //
                if (erroredDefaultToRemove.Count > 0)
                {
                    foreach (FieldDefaultValue value in erroredDefaultToRemove)
                    {
                        defaultsForThisWorkItem.Remove(value);
                    }
                    // Save the changed list of defaults.
                    //
                    FieldDefaultValueConfig.Instance.SetDefaultsForWorkItem(wi.Type, defaultsForThisWorkItem);
                }
            }
        }

        /// <summary>
        /// Returns true if the passed field decriptor is suitable for storing 
        /// the browser details string.
        /// </summary>
        /// <param name="fd">The field description to inspect</param>
        /// <returns>True if the field looks good.</returns>
        internal static bool IsFieldSutableToStoreDescription(FieldDefinition fd)
        {
            bool suitable = false;
            if (fd != null && fd.IsEditable &&
                (fd.FieldType == FieldType.PlainText || fd.FieldType == FieldType.Html))
            {
                suitable = true;
            }

            return suitable;
        }

        /// <summary>
        /// Returns true if the passed field is suitable for storing the browser
        /// details string.
        /// </summary>
        /// <param name="field">The field to inspect</param>
        /// <returns>True if the field looks good.</returns>
        internal static bool IsFieldSutableToStoreDescription(Field field)
        {
            bool suitable = false;

            if (field != null &&
                field.IsLimitedToAllowedValues == false &&
                field.HasPatternMatch == false)
            {
                suitable = IsFieldSutableToStoreDescription (field.FieldDefinition);
            }

            return suitable;
        }
    }
}
